package main

import (
	"errors"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/xuri/excelize"
)

type record struct {
	commiter    string
	commit_time string
	stu_no      int
	stu_name    string
	pic_file    []string
}
type fileList struct {
	path     string
	fileInfo os.FileInfo
}

var dirList []fileList
var inputXls = "000-学生健康码和行程卡上报（收集结果）.xlsx"

var outDir = "./out"
var inDir = "./"

func main() {
	infile := flag.String("infile", "", "input xls file name")
	flag.StringVar(&outDir, "outdir", "./out", "output directory")
	flag.Parse()
	inputXls = *infile
	inDir = filepath.Dir(inputXls)
	fmt.Println("infile: " + inputXls)
	fmt.Println("indir: " + inDir)
	fmt.Println("outdir: " + outDir)

	recs := readXls(inputXls)    //read data from xls file
	recs = getLatestRecord(recs) //for those duplicated records, only get the latest one
	usFN(&recs)                  //把文件名中的非法字符替换为下划线
	//fmt.Println(recs)

	init_dirList()     //读取输入目录下的所有文件
	initOutDir(outDir) //清空输入目录
	/*for _, fl := range dirList {
		fmt.Println(fl.fileInfo.Name())
	}*/
	//fn, err := recs[0].getFileName()
	for _, rec := range recs {
		err := rec.copyFile()
		if err != nil {
			fmt.Println(err)
			fmt.Println(strStuNo(rec.stu_no) + rec.stu_name + " Error!")
		} else {
			fmt.Println(strStuNo(rec.stu_no) + rec.stu_name + " OK")
		}
	}
}

//get latest record
func getLatestRecord(recs []record) []record {
	d := make(map[int]record)
	for _, rec := range recs {
		if oldRec, found := d[rec.stu_no]; found { //have old record
			if compTimeStr(rec.commit_time, oldRec.commit_time) {
				d[rec.stu_no] = rec //replace with new record
			}

		} else { //new record,just add
			d[rec.stu_no] = rec
		}
	}
	var keys []int
	for k := range d {
		keys = append(keys, k)
	}
	sort.Ints(keys)
	var outRecs []record
	for _, k := range keys {
		outRecs = append(outRecs, d[k])
	}
	return outRecs
}

//compare time string, if t1>t2,return true
func compTimeStr(t1, t2 string) bool {
	timeTemplate := "2006/1/02 15:04:05" //其他类型
	s1, err := time.ParseInLocation(timeTemplate, t1, time.Local)
	if err != nil {
		fmt.Print(err)
		os.Exit(1)
	}
	s2, err2 := time.ParseInLocation(timeTemplate, t2, time.Local)
	if err2 != nil {
		fmt.Print(err2)
		os.Exit(1)
	}
	st1 := s1.Unix()
	st2 := s2.Unix()
	if st1 > st2 {
		return true
	}
	return false
}

//从excel文件读取记录
func readXls(inputXls string) []record {
	var recs []record
	xlsx, err := excelize.OpenFile(inputXls)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	// Get all the rows in a sheet.
	rows, err := xlsx.GetRows(xlsx.GetSheetName(0))
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	/*for _, row := range rows {
		for _, colCell := range row {
			fmt.Print(colCell, "\t")
		}
		fmt.Println()
	}*/
	err = formatError(rows[0])
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	fileCols := getFileColumns(rows[0])
	for i := 1; i < len(rows); i++ {
		var rec record
		var files []string
		rec.commiter = rows[i][0]
		rec.commit_time = rows[i][1]
		j, err := strconv.Atoi(rows[i][2])
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		rec.stu_no = j
		rec.stu_name = rows[i][3]
		for _, idx := range fileCols {
			files = append(files, rows[i][idx])
		}
		rec.pic_file = files
		recs = append(recs, rec)
	}
	return recs
}

//check header
func formatError(row []string) error {
	if !strings.Contains(row[0], "提交者") {
		return fmt.Errorf("第一列名称'%s'不正确，必须为'提交者'")
	}
	if !strings.Contains(row[1], "提交时间") {
		return fmt.Errorf("第二列名称'%s'不正确，必须为'提交时间'")
	}
	if !strings.Contains(row[2], "学号") {
		return fmt.Errorf("第三列名称'%s'不正确，必须为'学号'")
	}
	if !strings.Contains(row[3], "姓名") {
		return fmt.Errorf("第四列名称'%s'不正确，必须为'姓名'")
	}
	return nil
}

//get file columns
func getFileColumns(row []string) []int {
	var fileCols []int
	for i := 4; i < len(row); i++ {
		if isFile(row[i]) {
			fileCols = append(fileCols, i)
		}
	}
	return fileCols
}

//列名中包含“图片”，，”截图“，”截屏“字样的，视为文件列
func isFile(col string) bool {
	var kws []string = []string{"图片", "截图", "文件", "截屏"}
	for _, kw := range kws {
		if strings.Contains(col, kw) {
			return true
		}
	}
	return false
}

//判断文件或目录是否存在
func PathExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, err
}

//clear output directory
func initOutDir(outPath string) error {
	flag, err := PathExists(outPath)
	if flag { //dir exists, remove it
		err2 := os.RemoveAll(outPath)
		if err2 != nil {
			fmt.Println(err2)
			os.Exit(1)
		}
	}
	if err == nil { //dir not exists, create it
		err2 := os.MkdirAll(outPath, 0777)
		if err2 != nil {
			fmt.Println(err2)
			os.Exit(1)
		}
	}
	return nil
}

//copy file
func cf(src, dst string) (int64, error) {
	sourceFileStat, err := os.Stat(src)
	if err != nil {
		return 0, err
	}
	//fmt.Println("Copying " + src)
	if !sourceFileStat.Mode().IsRegular() {
		return 0, fmt.Errorf("%s is not a regular file", src)
	}
	source, err := os.Open(src)
	if err != nil {
		return 0, err
	}
	defer source.Close()
	file_exist, err := PathExists(filepath.Dir(dst))
	if err != nil {
		return 0, err
	}
	if !file_exist {
		err2 := os.MkdirAll(filepath.Dir(dst), 0777)
		if err2 != nil {
			return 0, err2
		}
	}

	destination, err := os.Create(dst)
	if err != nil {
		return 0, err
	}
	defer destination.Close()
	nBytes, err := io.Copy(destination, source)
	return nBytes, err
}

//convert student number from int to 2-digit number
func strStuNo(n int) string {
	pre := ""
	if n < 10 {
		pre = "0"
	}
	return pre + strconv.Itoa(n)
}

//copy file for record
func (rec *record) copyFile() error {
	sourceFile, err := rec.getFileName()
	if err != nil {
		return err
	}
	for _, sf := range sourceFile {
		destFile := outDir + "/" + strStuNo(rec.stu_no) + rec.stu_name + "/" + sf
		nBytes, err := cf(inDir+"/"+sf, destFile)
		if err != nil {
			return err
		}
		if nBytes <= 0 {
			fmt.Println("Error:empty file " + sf)
		}
	}
	return nil
}

//change illegal characters(?/\*<>"|:) to underscore in filename
func usFilename(fn string) string {
	const Illchars = "?/\\*<>\"|:"
	for _, c := range Illchars {
		fn = strings.Replace(fn, string(c), "_", -1)
	}
	return fn

}

//change illegal characters in record
func (rec *record) underScoreFileName() {
	for i := 0; i < len(rec.pic_file); i++ {
		rec.pic_file[i] = usFilename(rec.pic_file[i])
		//fmt.Println(rec.pic_file[i])
	}
}

//change illegal characters in records
func usFN(recs *[]record) {
	for i := 0; i < len(*recs); i++ {
		(*recs)[i].underScoreFileName()
	}
}

func init_dirList() {
	//dir := "./"
	dir := inDir
	var err error
	dirList, err = readDir(dir)
	if err != nil {
		fmt.Println("readdir error:", err)
		return
	}
}

//递归遍历目录下所有文件
func readDir(dir string) (data []fileList, err error) {
	//判断文件或目录是否存在
	file, err := os.Stat(dir)

	if err != nil {
		return data, err

	}
	//如果不是目录，直接返回文件信息

	if !file.IsDir() {
		data = append(data, fileList{filepath.Dir(dir) + "/", file})
		return data, err
	}
	//如果是目录，读取目录下的所有文件
	fileInfo, err := ioutil.ReadDir(dir)
	if err != nil {
		fmt.Println(fileInfo)
		return data, err
	}
	//目录为空
	if len(fileInfo) == 0 {
		return
	}
	/*for _, v := range fileInfo {
		if v.IsDir() {
			if subDir, err := readDir(dir + v.Name()); err != nil {
				return data, err
			} else {
				data = append(data, subDir...)
			}

		} else {
			data = append(data, fileList{strings.TrimRight(dir, "/") + "/", v})
		}

	}*/
	//only process files in current directory, ignore subdir
	for _, v := range fileInfo {
		if !v.IsDir() {
			data = append(data, fileList{strings.TrimRight(dir, "\\") + "\\", v})
		}

	}
	return data, err
}

//查找匹配的文件名，要求有且仅有一个匹配，否则报错
func __getFileName(kw string) (string, error) {
	c := 0 //match count
	var mfn string
	var err error
	for _, file := range dirList {
		fn := file.fileInfo.Name()
		if strings.Contains(fn, kw) {
			c++
			mfn = fn
		}
	}
	if c == 0 {
		err = errors.New("No match for: " + kw)
	} else if c > 1 {
		err = errors.New(strconv.Itoa(c) + " matches for: " + kw + " (match must be 1 and only)")
	} else {
		err = nil
	}
	return mfn, err
}

//查找所有文件的匹配文件名
func (rec *record) getFileName() ([]string, error) {
	var res []string
	for _, fn := range rec.pic_file {
		//fmt.Println("searching " + fn)
		matchFile, err := __getFileName(fn)
		if err != nil {
			return res, err
		}
		res = append(res, matchFile)
	}
	return res, nil
}
